package world.square;

import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import world.Observer;
import world.edges.Border;
import world.edges.Door;
import world.edges.Wall;
import world.placement.Direction;
import be.kuleuven.cs.som.annotate.Basic;
import be.kuleuven.cs.som.annotate.Immutable;
import be.kuleuven.cs.som.annotate.Model;
import be.kuleuven.cs.som.annotate.Raw;
/**
 * A class representing a square, a small part inside a dungeon.
 * Each square has a certain temperature, humidity, inhabitability and borders.
 * 
 * @invar	The temperature must be a valid number.
 * 		  	| canHaveAsTemperature(getTemperature(),TemperatureScale.CELCIUS)
 * @invar 	The humidity must be a valid number.
 * 		  	| isValidHumidity(getHumidity())
 * @invar 	The minimum and maximum temperature are valid numbers.
 * 		  	| canHaveAsMinMaxTemperature(getMinTemperature(),getMaxTemperature(),TemperatureScale.CELCIUS) 
 * @invar 	The 'span' value for heat damage calculation is valid.
 * 		  	| isValidSpan(getHeatDamageSpan())
 * @invar 	The boundary value for heat damage calculation is valid.
 * 		  	| canHaveAsHeatDamageBoundary(getHeatDamageBoundary())
 * @invar 	The constant for the calculation of a weighted mean, must be valid
 * 		  	| isValidConstant(getConstantWeightedMean())
 * @invar	The square had proper borders.
 * 			| hasProperBorders()
 * @author Frederic Mes, Wim Marynissen
 * @version 3.0
 */
public class Square implements Subject{	
	
	/**
	 * Variable registering the temperature in degrees celcius of the square.
	 */
	private Temperature temperature;
	/**
	 * Variable registering the humidity of the square.
	 */
	private BigDecimal humidity;
	
	/**
	 * Initialize a square with a given temperature and humidity.
	 * @param	temperature
	 * 			The temperature of the new square.
	 * @param	humidity
	 * 			The humidity of the new square
	 * @param	direction
	 * 			The direction in which to add a wall.
	 * @pre		The new value for humidity must be valid.
	 * 			| isValidHumidity(humidity)
	 * @post	The new temperature is equal to the specified temperature.
	 * 			| new.getTemperature() == temperature
	 * @post	The new humidity is equal to the specified humidity.
	 * 			| new.getHumidity() == humidity
	 * @post	The boundary for heat damage is set to default value  //onzeker
	 * 			| new.getHeatDamgeBoundary() == 35
	 * @post	The span for heat damage is set to default value
	 * 			| new.getHeatDamgeSpan() == 15
	 * @post    The new square is initialized with the given constant used in
	 * 			the calculations of the weighted mean.
	 *        	| new.getConstantAverageMean() == constantAverageMean
	 * @post	Walls are added in the specified directions
	 * 			| for each direction in directions:
	 * 			| 		new.getBorders().contains(direction)
	 * @throws	IllegalArgumentException
	 * 			The provided temperature value is invalid.
	 * 			| !isValidTemperature(temperature)
	 * @throws	IllegalArgumentException
	 * 			The provided temperature scale is invalid.
	 * 			| !isValidTemperatureScale(scale)
	 * @throws	IllegalArgumentException
	 * 			The provided heat damage boundary is invalid.
	 * 			| !canHaveAsHeatDamageBoundary(heatDamageBoundary)
	 * @throws	IllegalArgumentException
	 * 			The provided heat damage span is invalid.
	 * 			| !isValidSpan(heatDamageSpan,getMaxTemperature().getValue(TemperatureScale.CELCIUS)
	 */
	public Square(Temperature temperature, BigDecimal humidity, Direction... directions ) throws IllegalArgumentException {
		this.minTemperature = new Temperature(-200,TemperatureScale.CELCIUS);
		this.maxTemperature = new Temperature(5000,TemperatureScale.CELCIUS);
		setTemperature(temperature);
		setHumidity(humidity);
		setHeatDamageBoundary(new Temperature(35,TemperatureScale.CELCIUS));
		setHeatDamageSpan(15);
		Square.constantWeightedMean = new BigDecimal ("0.2");
		if (directions!= null)
			for(Direction direction : directions)
				addBorders(new Border(new Wall(true), direction));
	}

	/**
	 * Constructor which creates a default square, with borders in the specified directions.
	 *
	 * @effect  A new square is initialized with standard values for the temperature and humidity, and walls in the specified directions.
     *        | this(new BigDecimal ("25"), TemperatureScale.CELCIUS, new BigDecimal ("10"),directions) 
	 */
	public Square(Direction... directions){
		this (new Temperature(25,TemperatureScale.CELCIUS),new BigDecimal ("10"), directions);
	}
	
	/**
	 * Constructor without arguments. Creates a standard square,
	 * with a temperature of 25 degrees Celcius, humidity of 10% and a wall in the northern direction.
	 *
	 * @effect  A new square is initialized with standard values for the temperature and humidity
     *        | this(new BigDecimal ("25"), TemperatureScale.CELCIUS, new BigDecimal ("10"),Direction.NORTH) 
	 */
	public Square(){
		this (new Temperature(25,TemperatureScale.CELCIUS),new BigDecimal ("10"), Direction.NORTH);
	}

	/**
	 * Variable representing the minimum value for the temperature.
	 */
	private Temperature minTemperature;
	/**
	 * Variable representing the maximum value for the temperature.
	 */
	private Temperature maxTemperature;
	
	/**
	 * Returns the temperature of the square in the specified temperature scale.	
	 */
	@Basic @Raw
	public Temperature getTemperature(){ 
		return this.temperature;
	}

	/**
	 * @param 	minTemperature
	 * 				The new value for the minimum temperature.
	 * @post	The new minimum temperature is equal to the provided value.
	 * 			| new.getMinTemperature() == minTemperature
	 * @throws	IllegalArgumentException
	 * 			The provided minimum temperature is invalid
	 * 			| !canHaveAsMinMaxTemperature(minTemperature,getMaxTemperature())
	 */
	@Raw
	public void setMinTemperature(Temperature minTemperature) throws IllegalArgumentException{
		if(!canHaveAsMinMaxTemperature(minTemperature,getMaxTemperature()))
			throw new IllegalArgumentException("Improper minimum temperature");
		this.minTemperature = minTemperature;

	}

	/**
	 * @param 	maxTemperature
	 * 				The new value for the maximum temperature.
	 * @post	The new maximum temperature is equal to the provided value.
	 * 			| new.getMaxTemperature() == maxTemperature
	 * @throws	IllegalArgumentException
	 * 			The provided maximum temperature is invalid
	 * 			| !canHaveAsMinMaxTemperature(getMinTemperature(),maxTemperature)
	 */
	@Raw
	public void setMaxTemperature(Temperature maxTemperature) throws IllegalArgumentException {
		if(!canHaveAsMinMaxTemperature(getMinTemperature(),maxTemperature))
			throw new IllegalArgumentException("Improper minimum temperature");
		this.maxTemperature = maxTemperature;
	}


	/**
	 * @param 	temperature
	 * 			The new temperature for this square
	 * @post	The new temperature is equal to the provided value.
	 * 			| new.getTemperature() == temperature
	 * @throws	IllegalArgumentException
	 * 			The provided temperature value is invalid.
	 * 			| !canHaveAsTemperature(temperature, TemperatureScale.CELCIUS)
	 */
	@Raw
	public void setTemperature(Temperature temperature) throws IllegalArgumentException{
		if (!canHaveAsTemperature(temperature))
			throw new IllegalArgumentException("Improper Temperature");
		this.temperature = temperature;
	}
	
	/**
	 * @param 	temperature
	 * 			The new temperature for this square
	 * @post	The new temperature is equal to the provided value.
	 * 			| new.getTemperature() == temperature
	 * @throws	IllegalArgumentException
	 * 			The provided temperature value is invalid.
	 * 			| !canHaveAsTemperature(temperature, TemperatureScale.CELCIUS)
	 * @note	This method sets the temperature AND updates the temperature of 
	 * 			squares in thesame room.
	 */
	@Raw
	public void setTemperature(Temperature temperature,	boolean updateImmediately) {
		if (updateImmediately) {
			setTemperature(temperature);
			notifyObservers();
		}
	}

	/**
	 * Returns the humidity of the square.
	 */
	@Basic @Raw
	public BigDecimal getHumidity() {
		return this.humidity.setScale(2);
	}

	/**
	 * @param 	humidity
	 * 			The new value for the humidity.
	 * @pre		The given humidity must be a valid value.
	 * 			| isValidHumidity(humidity)
	 * @post	The new humidity is equal to the provided value rounded to a decimal number
	 * 			with 2 fractional digits.
	 * 			| let
	 * 			| 	roundedHumidity = humidity.round (getContextForScale2 (humidity))
	 * 			| in
	 * 			| 	new.getHumidity().equals (roundedHumidity)	
	 * @note	If the amount of fractional numbers is less than 2,
	 * 			the bigdecimal is set to 2 fractional numbers.
	 */
	@Raw
	public void setHumidity(BigDecimal humidity) {
		if (humidity.scale() != 2){
			humidity = humidity.round(getContextForScale2(humidity));
			humidity.setScale (2);
		}
		assert (isValidHumidity(humidity));
		this.humidity = humidity;
	}
	
	/**
	 * @param 	humidity
	 * 			The new value for the humidity.
	 * @pre		The given humidity must be a valid value.
	 * 			| isValidHumidity(humidity)
	 * @post	The new humidity is equal to the provided value rounded to a decimal number
	 * 			with 2 fractional digits.
	 * 			| let
	 * 			| 	roundedHumidity = humidity.round (getContextForScale2 (humidity))
	 * 			| in
	 * 			| 	new.getHumidity().equals (roundedHumidity)	
	 * @note	This method sets the humidity AND updates the humidity of 
	 * 			squares in thesame room.
	 */
	@Raw
	public void setHumidity(BigDecimal humidity, boolean updateImmediately){
		if (updateImmediately) {
			setHumidity(humidity);
			notifyObservers();
		}
	}
	
	/**
	 * Method to test if a value can be used to represent the temperature of the square,
	 * based on the minimum and maximum values for the temperature.
	 * 
	 * @param 	temperature
	 * 			The value to validate.
	 * @return	The temperature must be effective
	 * 			| if(temperature == null)
	 * 			|	then result == false;
	 * @return	True if the temperature is between the outer bounds.
	 * 			| result == (temperature.compareTo(getMaxTemperature())
	 * 			|				&& (temperature.compareTo(getMinTemperature()) >= 0)
	 */
	@Raw
	public boolean canHaveAsTemperature(Temperature temperature){
		if(temperature == null)
			return false;
		return (temperature.compareTo(getMaxTemperature()) <= 0) && (temperature.compareTo(getMinTemperature()) >= 0);
	}
	
	/**
	 * Method to test if certain minimum and maximum values for temperature are valid.
	 * 
	 * @param 	minTemperature
	 * 			The minimum value for the temperature
	 * @param 	maxTemperature
	 * 			The maximum value for the temperature
	 * @return	True if the minimum temperature is smaller than the maximum temperature,
	 * 			and the temperature value lies between these two bounds.
	 * 			| result == (minTemperature.compareTo(getTemperature()) <= 0) && (maxTemperature.compareTo(getTemperature()) >= 0)
	 */
	@Raw
	public boolean canHaveAsMinMaxTemperature(Temperature minTemperature, Temperature maxTemperature){
		return (minTemperature.compareTo(getTemperature()) <= 0) && (maxTemperature.compareTo(getTemperature()) >= 0);
	}
	
	/**
	 * Get the minimum temperature in degrees Celcius.
	 * @return 	The lowest possible value for the temperature.
	 */
	@Basic
	private Temperature getMinTemperature() {
		return this.minTemperature;
	}

	/**
	 * Get the maximum temperature in degrees Celcius.
	 * @return 	The highest possible value for the temperature.
	 */
	@Basic
	private Temperature getMaxTemperature() {
		return this.maxTemperature;
	}

	/**
	 * @param	humidity
	 * 			The humidity to check
	 * @return	True if the humidity is effective, between or equal to 0 and 100%
	 * 			and has a scale of 2.
	 * 			| result == ((humidity != null) && (humidity.compareTo(new BigDecimal("100")) <= 0)
	 * 			|			 && (humidity.compareTo(new BigDecimal("0"))) && (humidity.scale() == 2))
	 */
	public static boolean isValidHumidity(BigDecimal humidity){
		return (humidity != null) && (humidity.compareTo(new BigDecimal("100")) <= 0) 
				&& (humidity.compareTo(new BigDecimal("0"))) >= 0;
	}
	
	/**
	 * Return the damage over time due to extremely cold temperatures
	 * 
	 * @return	The damage due to exteme cold, calculated as 1 point per 10 degrees below -5 degrees Celcius
	 * 			| calculateDamage(getTemperature()getValue(TemperatureScale.CELCIUS),new BigDecimal(COLD_DAMAGE_BOUNDARY),COLD_DAMAGE_SPAN)
	 */
	public int getColdDamage(){
		return calculateDamage(getTemperature().getValue(TemperatureScale.CELCIUS),new BigDecimal(COLD_DAMAGE_BOUNDARY),COLD_DAMAGE_SPAN);
	}
	/**
	 * Variable declaring the temperature value above which there will be heat damage for each span above it.
	 */
	private static final int COLD_DAMAGE_BOUNDARY = -5;
	/**
	 * Variable declaring the temperature span that results in a unit of heat damage.
	 */
	private static final int COLD_DAMAGE_SPAN = -10;
	
	/**
	 * Return the damage over time due to extremely cold temperatures
	 * 
	 * @return	The damage due to extreme heat, calculated as 1 point per 15 degrees above 35 degrees Celcius
	 * 			| calculateDamage(getTemperature().getValue(TemperatureScale.CELCIUS),getHeatDamageBoundary().getValue(TemperatureScale.CELCIUS),getHeatDamageSpan())
	 */
	public int getHeatDamage(){
		return calculateDamage(getTemperature().getValue(TemperatureScale.CELCIUS),getHeatDamageBoundary().getValue(TemperatureScale.CELCIUS),getHeatDamageSpan());
	}
	
	/**
	 * Return the damage based on a given boundary and span.
	 * The boundary sets the limit from where damage is possible, the span is the number the given value has to cross to make that a damagepoint is added. 
	 * 
	 * @param 	unitValue
	 * 		  	The value which has to be checked.
	 * @param 	unitBoundary
	 * 		  	The limit from where damage is possible.
	 * @param 	span
	 * 		  	The number the given value has to cross to make that a damagepoint is added.
	 * 		  	If the span is positive, damagepoints can only be given if the unitValue is higher than the boundary.
	 * 		  	If the span is negative, damagepoints can only be given if the unitValue is lower than the boundary.
	 * 		  
	 * @return	The damage based on the given parameters.
	 * 			| let unitBoundary = new BigDecimal(boundary)
	 * 			| if (span > 0 && (unitValue.compareTo(unitBoundary)>0))
	 * 			|	then result == unitValue.subtract(unitBoundary).divide(damageSpan, RoundingMode.DOWN).intValue()
	 * 			| if (span < 0 && (unitValue.compareTo(unitBoundary) < 0))
	 * 			| 	then result == unitValue.subtract(unitBoundary).divide(damageSpan, RoundingMode.DOWN).intValue()
	 * 			| else
	 * 			|	then result == 0
	 * 
 	 * @throws 	IllegalArgumentException
     *         	One of the provided arguments is non-effective.
	 *         	| (unitValue == null)	|| (unitBoundary == null) || (span == null)
     * 
     * @throws 	IllegalArgumentException
     *         	The given span is equal to 0
     *         	| span == 0
	 * @note 	The damage is always rounded down.
	 */
	
	public int calculateDamage(BigDecimal unitValue, BigDecimal unitBoundary, int span){
		if (span == 0)
            throw new IllegalArgumentException("The span cannot be equal to 0.");
		if((unitValue == null)	|| (unitBoundary == null) )
			throw new IllegalArgumentException("Non-effective argument.");
		BigDecimal damageSpan = new BigDecimal(span);

		if(span > 0 && (unitValue.compareTo(unitBoundary)>0)){
			return unitValue.subtract(unitBoundary)
					.divide(damageSpan, RoundingMode.DOWN).intValue();
		}
		else if (span < 0 && (unitValue.compareTo(unitBoundary) < 0)){
			return unitValue.subtract(unitBoundary)
					.divide(damageSpan, RoundingMode.DOWN).intValue();
		}
		else{
			return 0;
		}
	}
	
	/**
	 * Variable declaring the temperature value above which there will be heat damage for each span above it.
	 */
	private Temperature heatDamageBoundary;
	
	/**
	 * Variable declaring the temperature span that results in a unit of heat damage.
	 */
	private int heatDamageSpan;
		
	/**
	 * Returns the boundary for heat damage of the square.
	 */
	@Basic
	public Temperature getHeatDamageBoundary() {
		return heatDamageBoundary;
	}

	/**
	 * @param 	heatDamageBoundary
	 * 			The new value for the heat damage boundary.
	 * @post	The new boundary for heat damage is equal to the provided value.
	 * 			| new.getHeatDamageBoundary() == heatDamageBoundary
	 * @throws	IllegalArgumentException
	 * 			The provided temperature value is invalid.
	 * 			| !canHaveAsHeatDamageBoundary(heatDamageBoundary)
	 */
	public void setHeatDamageBoundary(Temperature heatDamageBoundary) {
		if (!canHaveAsHeatDamageBoundary(heatDamageBoundary)){
			throw new IllegalArgumentException("Improper value for heat damage boundary.");
		}
		this.heatDamageBoundary = heatDamageBoundary;
	}
	
	/**
	 * Method to test if a value can be used as 'span' for damage calculation.
	 * 
	 * @param 	heatDamageBoundary
	 * 			The maximum value the unit (temperature, humidity,...) can reach
	 * @return	True if the boundary is a valid value for temperature and bigger than the cold damage boundary.
	 * 			| let 
	 * 			|	boundary = new BigDecimal(heatDamageBoundary)
	 * 			| in
	 * 			| 	result == isValidTemperature(boundary) && heatDamageBoundary > COLD_DAMAGE_BOUNDARY
	 */
	public boolean canHaveAsHeatDamageBoundary(Temperature heatDamageBoundary){ 
		return canHaveAsTemperature(heatDamageBoundary) && (heatDamageBoundary.compareTo(new Temperature(COLD_DAMAGE_BOUNDARY,TemperatureScale.CELCIUS)) >= 0);
	}
	
	/**
	 * Returns the span of temperature that resembles 1 unit of heat damage.
	 */
	@Basic
	public int getHeatDamageSpan() {
		return heatDamageSpan;
	}

	/**
	 * @param 	heatDamageSpan
	 * 			The new value for the span of heat damage
	 * @post	The new span is equal to the provided value.
	 * 			| new.getHeatDamageSpan() == heatDamageSpan
	 * @throws	IllegalArgumentException
	 * 			The provided heat damage span is invalid.
	 * 			| !isValidSpan(heatDamageSpan,getMaxTemperature().getValue(TemperatureScale.CELCIUS))
	 */
	public void setHeatDamageSpan(int heatDamageSpan) throws IllegalArgumentException{ 
		if (!isValidSpan(heatDamageSpan,getMaxTemperature().getValue(TemperatureScale.CELCIUS))){
			throw new IllegalArgumentException();
		}
		this.heatDamageSpan = heatDamageSpan;
	}

	/**
	 * Method to test if a value can be used as 'span' for damage calculation.
	 * 
	 * @param 	span
	 * 			The span value to validate.
	 * @param 	maximumValue
	 * 			The maximum value the unit (temperature, humidity,...) can reach
	 * @return	True if the absolute value of the span is smaller than or equal to the maximum value for the unit.
	 * 			| maximumValue.compareTo(new BigDecimal(Math.abs(span))) >= 0
	 * @note 	Negative span values indicate that we count downward for damage calculation.
	 */
	public boolean isValidSpan(int span, BigDecimal maximumValue){ 
		return maximumValue.compareTo(new BigDecimal(Math.abs(span))) >= 0;
	}
	
	/**
	 * Return the damage over time to a metal object due to high humidity
	 * 
	 * @return	The damage due to rust, calculated as 1 point per 7 percent humidity above 30 percent
	 * 			| result == calculateDamage (getHumidity(), 30, 7)
	 */
	public int getRustDamage(){ 
		return calculateDamage(getHumidity(),new BigDecimal("30"),7);
	}
	
	/**
     * Returns if this square has a slippery floor
     * @return hasSlipperyFloor
     */
    @Basic
    public boolean getHasSlipperyFloor() {
            return hasSlipperyFloor;
    }
    /**
     * Sets the state of the boolean hasSlipperyFloor.
     * @param 	hasSlipperyFloor
     * 		  	Has got the square a slippery floor or not.
     * @post	The boolean 'hasSlipperyFloor' indicates whether the floor is slippery or not.
     * 		 	(new this).getHasSlipperyFloor() == hasSlipperyFloor
     */
    public void setHasSlipperyFloor(boolean hasSlipperyFloor) {
		this.hasSlipperyFloor = hasSlipperyFloor;
	}

	/**
	 * Check if this square is slippery
	 * 
	 * @return	A boolean to indicate if the floor is slippery. There are three different ways for a square to be slippery:
	 * 	- It is made of a slippery material
	 * 	- The humidity is 100% and the temperature is positive
	 * 	- The humidity is bigger than 10% and the temperature is negative
	 * 			| result == (hasSlipperyFloor 
	 * 			|				|| (getHumidity().intValue() == 100 && (getTemperature().compareTo(BigDecimal.ZERO) > 0) 
	 * 			|				|| ((getTemperature().compareTo(BigDecimal.ZERO) < 0) && getHumidity() > 10))
	 */
	public boolean isSlippery(){
		return (hasSlipperyFloor || ((getHumidity().intValue() == 100) && (getTemperature().compareTo(Temperature.ZERO) > 0)) || ((getTemperature().compareTo(Temperature.ZERO) < 0) && (getHumidity().compareTo(new BigDecimal("10"))) > 0));
	}
	
	/**
	 * A variable registering whether the floor is slippery.
	 */
	private boolean hasSlipperyFloor;	 


	/**
	 * This method returns the degree of inhabitability
	 * 
	 * @return  The resulting inhability is effective.
	 *        	| result != null 
	 * @return  The precision of the result is equal to 2.
	 *        	| result.getPrecision() == 2 
	 * @return  The division and therefor the result uses HALF_DOWN as its rounding mode.       
	 *        	| result.getRoundingMode() == RoundingMode.HALF_DOWN
	 */
	public BigDecimal inhabitability (){
		BigDecimal valuePart1 = sqrt(new BigDecimal (getHeatDamage()).pow(3));
		BigDecimal valuePart2 = valuePart1.divide(sqrt(new BigDecimal ("101").subtract(getHumidity())),RoundingMode.HALF_DOWN);
		BigDecimal valuePart3 = sqrt (new BigDecimal (getColdDamage()));
		BigDecimal result = (valuePart2.add(valuePart3)).negate();
		return result.round(getContextForScale2(result));
	}
	
	/**
	 * A list registering the borders of the square.
	 */	
	private ArrayList<Border> borders = new ArrayList<Border>();
	
	
	/**
	 * Add a number of borders to the square.
	 * 
	 * @param 	borders
	 * 			The borders to add.
	 * @post   	If the given borders are effective and the square isn't already bordered in the given direction(s),
	 *		  	the border(s) are added to the square. 
	 *       	| if ( (borders != null) && !this.isBorderedInDirection(border))
	 *       	|   then new.isBorderedInDirection(border) == true
	 * @post	If a border is added to the square, this square is also added to the border.
	 * 			| for each border in borders:
	 * 			| 	if (this.getBorder(border.getDirection()) == border)
	 * 			| 		then border.getSquare() == this
	 */
	@Raw
	public void addBorders (Border... borders){ 
		if (borders != null){
			for (Border border : borders){
				if(canHaveAsBorder(border)){
					if(isBorderedInDirection(border.getDirection()))
						this.borders.remove(getBorder(border.getDirection()));
					this.borders.add(border);
					border.setSquare(this);
				}
			}
			notifyObservers();
		}
	}
	
	
	/**
	 * Test if the square can have a certain border.
	 * @param 	border
	 * 			The border to check.
	 * @return	False if the border is non-effective or terminated.
	 * 			| if(border == null)
	 * 			| 	then result == false
	 * 			| if(border.isTerminated())
	 * 			| 	then result == false
	 * @return	If the border has a different squared assigned to it, return false.
	 * 			| if(!border.getSquare().equals(this) || border.getSquare() == null)
	 * 			| 	then result == false
	 * @return	False if the border contains a door in the direction
	 * 			of the floor or the ceiling.
	 * 			| if(border.getBorderElement() instanceof Door 
	 * 			|		&& ((border.getDirection() == Direction.FLOOR)
	 * 			| 				|| (border.getDirection() == Direction.CEILING))
	 * 			|		then result == false
	 * @return	The amount of doors must be smaller than the limit.
	 * 			| result == ((countDoors() < maxNbDoors()) || (maxNbDoors() == 0))
	 */
	@Raw
	public boolean canHaveAsBorder(Border border){
		if (border == null)
			return false;
		if (border.isTerminated()){
			return false;
		}
		if(border.getSquare() != null)
			if(!border.getSquare().equals(this))
				return false;
		if(border.getBorderElement() instanceof Door)
			if((border.getDirection() == Direction.FLOOR) || (border.getDirection() == Direction.CEILING))
				return false;
		return ((countDoors() < maxNbDoors()) || (maxNbDoors() == 0));
		
	}
	
	/**
	 * Count the amount of doors.
	 * @return	The number of doors of this square.
	 */
	public int countDoors(){
		int amountOfDoors = 0;
		for (Border bord : borders){
			if (bord.getBorderElement() instanceof Door){
				amountOfDoors++;
			}
		}
		return amountOfDoors;
	}

	/**
	 * @param 	direction
	 * 			Direction in which the border at that direction should be validated.
	 * @return	True if the square has no border in a certain direction.
	 * 			| if(getBorder(direction) == null)
	 * 			| 	then result == true
	 * @return	False if the square associated with the border in the given direction,
	 * 			does not refer back to this square.
	 * 			| if(getBorder(direction) != null)
	 * 			| 	then result == (getBorder(direction).getSquare() == this
	 */
	public boolean hasProperBorder(Direction direction){
		if( getBorder(direction) != null ){
			return (getBorder(direction).getSquare() == this);
		}
		return true;
		
	}

	/**
	 * Remove borders in the specified directions.
	 * @param	directions
	 * 			The directions in which to remove the bordering elements.
	 * @post 	If the given directions are effective and the square can remove the border
	 * 			in the provided direction then the border will be removed.
	 * 			| for each direction in directions:
	 *       	| 	if (canRemoveBorderInDirection (direction))
	 *       	|   	then new.isBorderedIn(direction) == false
	 * @post	If the border can be removed from the square,
	 * 			the square will also be removed from the border.
	 * 			| if (canRemoveBorderInDirection (direction))
	 * 			|	then getBorder(direction).removeSquare()
	 * @note 	multiple borders can be removed at once.
	 */
	public void removeBorders(Direction... directions){
		if(directions != null){
			for (Direction direction : directions){
				if (canRemoveBorderInDirection (direction)){
					getBorder(direction).removeSquare();
					this.getBorder(direction).terminate();
				}
			}
			notifyObservers();
		}
	}
	
	/**
	 * Remove a border from this square.
	 * 
	 * @param 	border
	 * 			The border to be removed.
	 * @throw	IllegalArgumentException
	 * 			The border can't be removed.
	 * 			| !canRemoveBorderInDirection(border.getDirection())
	 */
	public void removeBorder(Border border) throws IllegalArgumentException{
		if(border != null){
			if(!canRemoveBorderInDirection(border.getDirection()))
				throw new IllegalArgumentException("Can't remove that border.");
			this.borders.remove(border);
		}
	}
	
	/**
	 * @param 	direction
	 * 			The direction in which to check for possibility of removal.
	 * @return	False if the direction is non-effective or there's no border in that direction.
	 * 			| if ((direction == null) || (getBorder(direction) == null))
	 * 			|	then result == false
	 * @return	If there is only one border, return true only if this square is terminated.
	 * 			| if(borders.size() == 1)
	 * 			|	then result == isTerminated()
	 */
	public boolean canRemoveBorderInDirection(Direction direction){
		if ((direction == null) || (getBorder(direction) == null))
			return false;
		if(getBorders().size() == 1)
				return isTerminated();
		return true;
	}
	
	
	/**
	 * A method to check whether the square is bordered in a given direction.
	 * @param	direction
	 * 			Direction to check for a border.
	 * @return	True if the square has border in the given direction
	 * 			| if(getBorder(direction) != null)
	 * 			| 	then result == true
	 */
	public boolean isBorderedInDirection(Direction direction){
		if ((getBorder(direction) == null) || (getBorder(direction).isTerminated()))
			return false;
		return true;
	}
	
	/**
	 * Returns the borders of this square.
	 */
	public ArrayList<Border> getBorders (){
		return borders;
	}
	
	/**
     * Return the border of this square in the given direction. 
     * If there is nothing in the given direction, the method will return null.
     * 
     * @param 	direction
     *         	The direction of border to return.
     * @return	The border in the provided direction.
     * 			| for border in getBorders():
     * 			| 	if (border.getDirection() == direction)
     * 			| 		then result == border 
     * @throws	IllegalArgumentException
     * 			The provided direction is non-effective
     * 			| direction == null
     */
    @Raw 
    public Border getBorder(Direction direction) throws IllegalArgumentException {
    	if(direction == null)
    		throw new IllegalArgumentException("Non-effective direction");
    	for(int i=0; i<getBorders().size(); i++){
			if (getBorders().get(i).getDirection() == direction){
				return getBorders().get(i);
			}
		}
		return null;
    }
   
    
    /**
     * Check if the square has proper borders.
     * 
     * @return	The square has a proper border in each direction.
     * 			| for each direction in Direction.values():
     * 			| 	if(!hasProperBorder(direction))
     * 			|		then result == false
     * @return	The amount of doors must be at least 1 and smaller than the maximum..
     * 			| result == ((countBorders() >= 1) && (countDoors() <= maxNbDoors()))
     */
    @Raw
    public boolean hasProperBorders(){
        for(Direction direction : Direction.values()){
            if(!hasProperBorder(direction))
                    return false;
        }     
        return ((countBorders() >= 1) && (countDoors() <= maxNbDoors()));    
    }
 
    /**
	 * Count the amount of borders.
	 * @return	The number of borders of this square.
	 */
	public int countBorders(){
		int amountOfBorders = 0;
		for (Direction direction : Direction.values()){
			if(isBorderedInDirection(direction))
				amountOfBorders++;
		}
		return amountOfBorders;
	}

	/**
	 * Makes this square merge with the given square, in the given direction.
	 * 
	 * @param 	square
	 * 			The square this square has to be merged with.
	 * @param 	Direction
	 * 			The direction in which the squares have to be merged. 
	 * 
	 * @post 	The new humidity of both squares is equal to the average of both humidities.
	 * 			|(new this).getHumidity() == calculateAverage (this.getHumidity(),square.getHumidity())
	 * 			|&& (new square).getHumidity() == calculateAverage (this.getHumidity(),square.getHumidity())

	 * @post 	The new temperature of both squares is equal to the weighted mean of both temperatures.
	 * 		 	The new temperature will be bigger than the smallest of the temperatures of both squares, 
	 * 			and smaller than the largest temperature.
	 * 			|(new this).getTemperature() >= Math.min (this.getTemperature(), square.getTemperature())
	 * 			| && ((new this).getTemperature() <= Math.max (this.getTemperature(), square.getTemperature()
	 * 
	 * @post	Both squares will have thesame temperature
	 * 			| (new this).getTemperature() == (new square).getTemperature()
	 * 	
	 * @post 	Both squares do not have an border object in the given direction.
	 * 			|!(new this).isBorderedBy (Direction) && !(new square).isBorderedBy (Direction)
	 * 
	 * @throws 	IllegalArgumentException
	 *			The given square to merge with is not effective.
	 *			|square == null 
	 *
	 * @throws 	IllegalArgumentException
	 *			The given Direction is not effective.
	 *        	|Direction == null
	 * 
	 */
	public void mergeWith (Square square, Direction Direction) throws IllegalArgumentException{
		if (square == null || Direction == null)
			throw new IllegalArgumentException("Non-effective argument(s).");
		
		BigDecimal averageHumidity = calculateAverage(this.getHumidity(), square.getHumidity());
		BigDecimal weightedMeanTemperature = calculateWeightedMean(square.getTemperature().getValue(TemperatureScale.CELCIUS), this.getTemperature().getValue(TemperatureScale.CELCIUS), square.getHumidity(), this.getHumidity());
		
		this.setHumidity(averageHumidity);
		square.setHumidity(averageHumidity);
		this.setTemperature(new Temperature(weightedMeanTemperature,TemperatureScale.CELCIUS));
		square.setTemperature(new Temperature(weightedMeanTemperature,TemperatureScale.CELCIUS));
		
		this.removeBorders(Direction);
		square.removeBorders(Direction.getOpposite());
		
	}
	
	/**
	 * Returns the weighted mean of 2 values, based on 2 (other) values.
	 *  
	 * The formula without the use of BigDecimal:
	 * (calculateWeights (value4, value3)*value1 + calculateWeights (value3, value4) * value2) / 2
	 * And with the use of BigDecimal:
	 * ((calculateWeights(value4, value3).multiply(value1)).add(calculateWeights(value3, value4).multiply(value2))).divide(new BigDecimal ("2"),RoundingMode.DOWN);
	 * 
	 * @param 	value1
	 * 		  	The first of two values of which has to be taken the weighed mean.	
	 * @param 	value2
	 * 		  	The second of two values of which has to be taken the weighed mean.
	 * @param 	value3
	 * 		  	The first of two values on which the calculations of the weights are based.
	 * @param 	value4
	 * 		  	The second of two values on which the calculations of the weights are based.
	 * @return 	The weighted mean of the first two values, based on the last two values.
	 * 		   	|result = ((calculateWeights(value4, value3).multiply(value1)).add(calculateWeights(value3, value4).multiply(value2))).divide(new BigDecimal ("2"),RoundingMode.DOWN);
	 * 
	 * @throws 	IllegalArgumentException
	 *			One of the provided values is non-effective
	 *        	|value1 == null	|| value2 == null || value3 == null || value4 == null  
	 * 		   
	 * @note The weighted mean is always in between of the first two values.
	 */
	
	public BigDecimal calculateWeightedMean(BigDecimal value1,BigDecimal value2,BigDecimal value3,BigDecimal value4) throws IllegalArgumentException{
		if (value1 == null || value2 == null || value3 == null || value4 == null)
			throw new IllegalArgumentException();
		return ((calculateWeight(value4, value3).multiply(value1)).add(calculateWeight(value3, value4).multiply(value2))).divide(new BigDecimal ("2"),RoundingMode.DOWN);		
	}
	
	/**
	 * Returns the average of two numbers, calculated as the sum of the two numbers divided by two.
	 * @param 	value1
	 * 		  	The first number
	 * @param 	value2
	 * 		  	The second number
	 * @return 	The average of the two numbers. In mathematical terms, we return: (value1+value2)/2
	 * 		   	result == (value1.add(value2)).divide(new BigDecimal ("2.00"),RoundingMode.DOWN)
	 * @throws 	IllegalArgumentException
	 *			value1 is non-effective.
	 *        	| value1 == null	
	 * @throws 	IllegalArgumentException
	 *			value2 is non-effective.
	 *        	| value2 == null	    
	 */
	
	public BigDecimal calculateAverage (BigDecimal value1, BigDecimal value2) throws IllegalArgumentException{
		if (value1 == null || value2 == null)
			throw new IllegalArgumentException("Non-effective Value");
		return (value1.add(value2)).divide(new BigDecimal ("2"));
	}
	
	/**
	 *Returns the calculated weight based on the two arguments. 
	 * 
	 * @param 	value1
	 * 		  	The first of two values based on which we calculate the weights
	 * @param 	value2
	 * 		  	The second of two values based on which we calculate the weights
	 * @return 	The calculated weight based on the two arguments. The result is the weight of the second argument.
	 * 		   	If both humidities are equal to 0, then both weights are equal to 1.
	 * 		   	| if (calculateAverage(value1, value2).doubleValue() == 0
	 * 		   	| 	then result == BigDecimal.ONE
	 * 		   	| else
	 * 		   	|	then result == weightWithoutConstant + getConstantWeightedMean()			
	 * @throws 	IllegalArgumentException
	 *			value1 is non-effective.
	 *        	| value1 == null	
	 * @throws 	IllegalArgumentException
	 *			value2 is non-effective.
	 *        	| value2 == null	
	 * @note Different results will be different if the order of the values is different.
	 * 		 It's important to put the value you want the weight of as the second argument.
	 * @note The calculations of the weights are system dependent, so the value of weightWithoutConstant can vary.
	 */
	public BigDecimal calculateWeight (BigDecimal value1, BigDecimal value2) throws IllegalArgumentException{
		
		BigDecimal average = calculateAverage(value1, value2);
		if (average.doubleValue() == 0)
			return BigDecimal.ONE;
		BigDecimal factor = (BigDecimal.ONE).subtract(getConstantWeightedMean());
		BigDecimal weightWithoutConstant = factor.multiply(value2.divide(average,RoundingMode.DOWN));
		BigDecimal weight = weightWithoutConstant.add(getConstantWeightedMean());

		return weight.round(getContextForScale2(weight));
	}

	
	/**
	 * Returns the constant used in the calculations of the weighted mean.
	 */
	@Basic
	public BigDecimal getConstantWeightedMean(){
		return constantWeightedMean;
	}
	
	/**
	 * Sets the new value of the constant used in the calculations of the weighted mean.  
	 * 
	 * @param 	newConstantWeightedMean
	 * 			The new constant used in the calculations of the weighted mean.
	 * @throws	IllegalArgumentException
	 * 			The constant is invalid
	 * 			|!isValidConstant(newConstantWeightedMean)
	 */

	public static void setConstantWeightedMean(BigDecimal newConstantWeightedMean) throws IllegalArgumentException{
		if(!isValidConstant(newConstantWeightedMean))
			throw new IllegalArgumentException("Invalid constant");
		constantWeightedMean = newConstantWeightedMean;
	}
	
	
	/**
	 * Variable registering the constant used in the calculations of the weighted mean.
	 */
	private static BigDecimal constantWeightedMean;
	
	/**
     * Check is the provided constant is a valid constant for weighted average for temperatures.
     * 
     * @param       constant
     *              The constant to validate
     *              
     * @return		The constant must be effective.
     * 				| if(constant == null)
     * 				|	then result == false
     * @return      True if constant is lower or equal to 0.4 or is higher or equal to 0.1
     *              | result =(constant.compareTo(new BigDecimal("0.1")) >= 0) 
     *              |			&& (constant.compareTo(new BigDecimal("0.4")) <= 0)
     */
    @Raw
    public static boolean isValidConstant(BigDecimal constant) {
            return (constant != null) &&(constant.compareTo(new BigDecimal("0.1")) >= 0) && (constant.compareTo(new BigDecimal("0.4")) <= 0);
    }

    /**
     * Terminate this square.
     * 
     * @post This square is terminated.
     *       | new.isTerminated() == true
     * @post If this square was not already terminated, this square no longer has obstacles.
     *       | if(!isTerminated())
     *       |	then for each direction in Direction.values():
     *       |			(new this).isBorderedInDirection(direction) == false
     * @post The borders of this square are terminated.
     * 		 | for each border in getBorders():
     * 		 | 		border.isTerminated() == true
     */
    public void terminate() {
    	if(!isTerminated()){
            this.isTerminated = true;
            for(Direction direction : Direction.values() ){
                if(this.isBorderedInDirection(direction)){
                    Border border = this.getBorder(direction);
                    removeBorders(direction);
                    border.terminate();
                }
            }
        }

    }


    /**
     * Check whether this square is terminated.
     */
    @Basic @Raw
    public boolean isTerminated() {
		return this.isTerminated;
	}
    
    /**
     * Variable registering whether this square is terminated.
     */
    public boolean isTerminated = false;

    
	/**
	 * Allows to make use of the square root operator on BigDecimals.
	 * 
	 * @param 	number
	 * 		  	The number we want to take the square root of.
	 * @return 	The square root of the given number.
	 * 		   	result == BigDecimal.valueOf(StrictMath.sqrt(number.doubleValue()));
	 * @throws 	IllegalArgumentException
	 * 			The given number is not effective.
	 * 			| number == null
	 * @throws 	NumberFormatException
	 * 			The given number is negative. 
	 * 			| number.compareTo (new BigDecimal ("0")) < 0
	 */
	public static BigDecimal sqrt(BigDecimal number) throws IllegalArgumentException,NumberFormatException{
		if(number == null)
			throw new IllegalArgumentException("Non-effective number");
		if(number.compareTo (new BigDecimal ("0")) < 0)
			throw new NumberFormatException("Can't calculate the square root of a negative number");
		return BigDecimal.valueOf(StrictMath.sqrt(number.doubleValue()));
	}
	
	/**
	 * Prints the state of this square, involving the temperature, humidity, slipperiness
	 * and borders.
	 */
	public void printState() {
		System.out.println("Temperatuur: " + getTemperature());
		System.out.println("Vochtigheidsgraad: " + getHumidity() + "%");
		if(isSlippery()){
			System.out.println("Glad");
		}
		else{
			System.out.println("Niet glad");
		}
		printBorders();
	}
		
	/** 
	   * Return a mathematical context to round the given big 
	   * decimal to 2 fractional digits. 
	   *  
	   * @param   	value 
	   *          	The value to compute a mathematical context for. 
	   * @pre    	The given value must be effective. 
	   *        	| value != null 
	   * @return  	The resulting mathematical context is effective. 
	   *        	| result != null 
	   * @return  	The precision of the resulting mathematical 
	   *          	context is equal to the precision of the given 
	   *          	value diminished with its scale and incremented 
	   *          	by 2. 
	   *        	| result.getPrecision() == 
	   *        	|   value.precision()-value.scale()+2 
	   * @return  	The resulting mathematical context uses UP
	   *          	as its rounding mode.       
	   *        	| result.getRoundingMode() == 
	   *        	|   RoundingMode.UP 
	   *        
	   * @note 		This exact code element is extracted from example 37 on p.297 from Prof. Steegmans'
	   *			course (Chapter 4 - Value Classes).
	   */ 
	@Model 
	static MathContext getContextForScale2(BigDecimal value) { 
	    assert value != null; 
	    return new MathContext(value.precision()-value.scale()+2, 
	                           RoundingMode.UP); 
	 }
	
	/**
	 * Print the borders of the square.
	 */
	public void printBorders (){
		System.out.println("Borders from square " + this.toString() + " = ");
		for (Border border : borders){
				System.out.println("  - Direction : " + border.getDirection().toString());
				System.out.println("  - Border : " + border.getBorderElement().toString());
		}
	}
	
	/**
	 * Prints the temperature and humidity
	 */
	public void printTempHum(){
		System.out.println("Temperatuur: " + getTemperature().getValue(TemperatureScale.CELCIUS)); 
		System.out.println("Humidity: " + getHumidity());
	}

	/**
	 * Add an observer in order to know when the square's state has been changed.
	 * Method used in the ObjectObserver pattern.
	 */
	public void addObserver(Observer o) {
		observers.add(o);		
	}

	/**
	 * Remove an observer from this subject.
	 * Method used in the ObjectObserver pattern.
	 */
	public void removeObserver(Observer o) {
		observers.remove(o);		
	}
	/**
	 * Notify the observer that an update is needed.
	 * Method used in the ObjectObserver pattern.
	 */
	public void notifyObservers() {
		   Iterator<Observer> i = observers.iterator();
		   while (i.hasNext()) {
		     Observer o = (Observer) i.next();
		     o.update(this);
		   }
		 }
	
	/**
	 * List containing the observers of this square.
	 */
	private List<Observer> observers = new ArrayList<Observer>();
	
	/**
	 * Get the maximum number of doors.
	 * @return
	 */
	@Basic @Immutable
	public int maxNbDoors() {
		return 3;
	}
}
